GitHub • Documentation • API • Demos
This notebook shows various animations created with ΦFlow.
To animate a plot, simply pass sequence data to vis.plot() and specify the time dimension using the animate argument.
All animations are rendered with Matplotlib and ffmpeg.
# !pip install --quiet phiflow
from phi.flow import *
We define the sine waves $\sin(x - t)$ and $\sin(x + t)$ and sample them on a grid from $x=0$ to $x=2\pi$ with resolution $R_x = 100$. This is done for 60 values of $t$, linearly spaced between $0$ and $4\pi$. These curves are animated in the left plot and their sum, a standing wave, is plotted on the right.
curves = CenteredGrid(lambda x, t: stack([math.sin(x - t), math.cos(x + t)], channel('c')), x=100, t=60, bounds=Box(x=2*PI, t=4*PI)).t[:-1]
plot({"Curves": curves, "Sum": sum(curves.c)}, animate='t')
<Figure size 640x480 with 0 Axes>
Geometric primitives like Sphere and Box can be plotted directly. Instance dimensions denote collections of objects.
plot(Sphere(x=wrap([0, 2], instance('s')), y=0, radius=math.linspace(0, 1, batch(t=50))**.5), animate='t')
<Figure size 640x480 with 0 Axes>
x = math.range(instance(boxes=10))
plot(Box(x=(x, x+1), y=(0, 2 * math.sin(math.linspace(0, 2*PI, batch(t=30)) + x*.5))), animate='t')
<Figure size 640x480 with 0 Axes>
In addition to the point locations, PointClouds can store per-point values, such as vectors.
x = math.rotate_vector(vec(x=1, y=0), angle=math.linspace(0, 2*PI, spatial(points=50)))
dx, x = x.points[1:] - x.points[:-1], x.points[:-1]
plot(vis.overlay(PointCloud(x, dx), rename_dims(PointCloud(x, dx, color='#40FFFF'), 'points', 'time')), animate='time')
/tmp/ipykernel_2004/4004326419.py:3: RuntimeWarning: rename_dims() default implementation is slow on large dimensions ((pointsˢ=49)). Please implement __replace_dims__() plot(vis.overlay(PointCloud(x, dx), rename_dims(PointCloud(x, dx, color='#40FFFF'), 'points', 'time')), animate='time')
<Figure size 640x480 with 0 Axes>
Here we visualize the built-in class Noise, sampling it on a $64^3$ grid ranging from 0 to 10 along each axis. We plot all $x$-$y$ slices over time, yielding a scanning animation.
The left plot shows noise with a smoothness of 1.0 and the right plot shows the same random noise (equal seed) with smoothness of 1.3.
noise = Noise(smoothness=stack({"Default Noise": 1.0, "Smooth Noise": 1.3}, batch('c')))
grid = CenteredGrid(noise, x=64, y=64, z=64, bounds=Box(x=10, y=10, z=10))
plot(grid, animate='z', show_color_bar=False)
<Figure size 640x480 with 0 Axes>
This animation shows two planets circling the sun, using a PointCloud with spherical elements for visualization.
PLANETS = instance(planets='Sun,Earth,Mars')
x = tensor([(0, 0), (9, 0), (0, 12)], PLANETS, channel(vector='x,y'))
x = math.rotate_vector(x, math.linspace(0, wrap([0, 5, 3], PLANETS), batch(time=130)))
plot(PointCloud(Sphere(x, radius=wrap([1, .4, .2], PLANETS))), animate='time')
<Figure size 640x480 with 0 Axes>
Two spheres are placed in a $32^3$ domain, at positions (16, 16, 0) and (16, 16, 32). Their radii grow linearly in time. These spheres are then sampled on a regular grid and plotted as voxels. Additionally, we plot the cross section $y=16$ as a 2D heat map.
sphere = Sphere(x=16, y=16, z=0, radius=math.linspace(0, 16, batch(time=17)))
grid = CenteredGrid(union(sphere, sphere.shifted((0, 0, 32))), x=32, y=32, z=32)
plot({"3D": grid, "2D Slice": grid.y[16]}, animate='time', frame_time=300)
<Figure size 640x480 with 0 Axes>
For these animated spirals, we plot 200 points whose distance increases linearly from the origin and whose angle increases linearly from 0 to $\alpha = 20 \frac{t}{T}$ where $t$ denotes the current frame and $T$ the number of frames.
When no geometric shape is specified, PointClouds are plotted as x.
dst = math.linspace(0, 1, instance(points=200))
angle = math.linspace(0, math.linspace(0, 20, batch(t=100)), dst.shape)
plot(PointCloud(dst * vec(x=math.cos(angle), y=math.sin(angle))), animate='t')
<Figure size 640x480 with 0 Axes>
Varying the parameters can produce vastly different patterns.
dst = math.linspace(0, 1, instance(points=200))
angle = math.linspace(0, math.linspace(PI*200, 1.1*PI*200, batch(t=200)), dst.shape)
plot(PointCloud(dst * vec(x=math.cos(angle), y=math.sin(angle))), animate='t')
<Figure size 640x480 with 0 Axes>
This demo visualizes the evolution of a PointCloud as a 3D scatter plot animation.
Thirty balls are placed at random locations. The initial velocities are sampled from a normal distribution with standard deviations $\sigma_x = \sigma_y = 1$ and $\sigma_z = 2$.
A simulation is than run for 100 frames, performing the following operations at each step:
x0 = math.random_uniform(instance(balls=30), channel(vector='x,y,z')) + 5
balls = PointCloud(Sphere(x0, radius=.1), math.random_normal(x0.shape) * (1, 1, 2))
def step(balls, dt=.1):
balls *= math.where(balls.points.vector['z'] < 0, (1, 1, -1), 1) * 0.7 ** dt
return advect.points(balls, balls, dt) + (0, 0, -9.81 * dt)
plot(iterate(step, batch(t=100), balls).mask(), animate='t')
<Figure size 640x480 with 0 Axes>
Burgers' equation is a partial differential equation consisting of an advection term and a diffusion term acting on a vector field $v$ (velocity). It reads
$$\frac{\partial v}{\partial t} = \nu \frac{\partial^2 v}{\partial x^2} - v \frac{\partial v}{\partial x}.$$Here, we simulate Burgers' equation on a $64^2$ grid for 100 time steps with $\Delta t = 0.5$, starting with a randomly generated initial condition. The evolution is plotted as a vector field. A standalone demo of Burgers' equation is also available here.
velocity = CenteredGrid(Noise(smoothness=1.5, vector='x,y'), extrapolation.PERIODIC, x=64, y=64) * 2
def burgers(v, dt=.5):
return diffuse.explicit(advect.semi_lagrangian(v, v, dt), .08, dt)
vis.plot(iterate(burgers, batch(time=100), velocity), animate='time')
<Figure size 640x480 with 0 Axes>
Next, we simulate an incompressible fluid with moderate diffusion. We split the PDE
$$\frac{\partial v}{\partial t} = \nu \frac{\partial^2 v}{\partial x^2} - v \frac{\partial v}{\partial x} - \nabla p \quad \mathrm{s.t.} \quad \nabla \cdot v = 0$$into advection, diffusion and pressure projection but will rely purely on numerical diffusion in this example. Starting from a random initial conditions, the fluid is simulated for 40 time steps and the vorticity $w = \nabla \times v$ and the pressure $p$ are shown. Also check out the tutorial notebook or the standalone Python scripts.
def incompressible_fluid_step(v: StaggeredGrid, p: CenteredGrid, dt=.5):
return fluid.make_incompressible(advect.advect(v, v, dt), (), Solve('auto', 1e-5, 1e-5, x0=p))
trj = iterate(incompressible_fluid_step, batch(time=40), *fluid.make_incompressible(StaggeredGrid(Noise(), 0, x=64, y=64)))
plot({"Vorticity": field.curl(trj[0]), "Pressure": trj[1]}, animate='time', same_scale=False)
<Figure size 640x480 with 0 Axes>
The incompressibility constraint $\nabla \cdot v$ in the Navier-Stokes equations is numerically achieved by solving the linear system of equations
$$\nabla p = \nabla \cdot v'$$which yields the pseudo-pressure $p$. This is typically done with a conjugate gradient solver using a laplace stencil (5-point in 2D, 7-point in 3D). This demo visualizes how the pressure optimization progresses internally for a tentative velocity $v' = (1, 1)$ inside a circle at the center of the $100^2$ domain and $v' = 0$ outside.
with math.SolveTape(record_trajectories=True) as solves:
fluid.make_incompressible(StaggeredGrid(Sphere(x=50, y=50, radius=20), 0, x=100, y=100))
plot(solves[0].x, animate='trajectory', frame_time=50)
<Figure size 640x480 with 0 Axes>
This simulation consists of two quantities $u$ and $v$ that interact via a non-linear partial differential equation (PDE) involving diffusion terms, $\nabla^2 u$ and $\nabla^2 v$. Depending on the exact form and parameters of the PDE, a myriad of resulting patterns can be achieved. The simulation is run for 1000 frames but we only plot every 10th since small time steps must be chosen for stability.
def reaction_diffusion(u, v, du=.19, dv=.05, f=.06, k=.062, dt=1.):
return u + dt * du * field.laplace(u) - u * v**2 + f * (1 - u), v + dt * dv * field.laplace(v) + u * v**2 - (f + k) * v
trj_u, trj_v = iterate(reaction_diffusion, batch(time=1000), *[CenteredGrid(Noise(scale=20, smoothness=1.3), x=100, y=100) * .2 + .5]*2)
plot(trj_u.time[::10], animate='time')
<Figure size 640x480 with 0 Axes>